import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import squarify
import folium
import json
import seaborn as sbn
from urllib.request import urlopen
import plotly.express as px
import warnings
warnings.filterwarnings('ignore')
fullData = pd.read_csv('valeursfoncieres-2020.txt', sep='|')
fullData2017 = pd.read_csv('valeursfoncieres-2017.txt', sep='|')
fullData2018 = pd.read_csv('valeursfoncieres-2018.txt', sep='|')
fullData2019 = pd.read_csv('valeursfoncieres-2019.txt', sep='|')
fullData2020 = pd.read_csv('valeursfoncieres-2020.txt', sep='|')
fullData2021 = pd.read_csv('valeursfoncieres-2021.txt', sep='|')
fullData
| Code service CH | Reference document | 1 Articles CGI | 2 Articles CGI | 3 Articles CGI | 4 Articles CGI | 5 Articles CGI | No disposition | Date mutation | Nature mutation | ... | Surface Carrez du 5eme lot | Nombre de lots | Code type local | Type local | Identifiant local | Surface reelle bati | Nombre pieces principales | Nature culture | Nature culture speciale | Surface terrain | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 1 | 07/01/2020 | Vente | ... | NaN | 0 | NaN | NaN | NaN | NaN | NaN | T | NaN | 1061.0 |
| 1 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 1 | 02/01/2020 | Vente | ... | NaN | 0 | NaN | NaN | NaN | NaN | NaN | BT | NaN | 85.0 |
| 2 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 1 | 02/01/2020 | Vente | ... | NaN | 0 | NaN | NaN | NaN | NaN | NaN | T | NaN | 1115.0 |
| 3 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 1 | 02/01/2020 | Vente | ... | NaN | 0 | NaN | NaN | NaN | NaN | NaN | T | NaN | 1940.0 |
| 4 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 1 | 02/01/2020 | Vente | ... | NaN | 0 | NaN | NaN | NaN | NaN | NaN | T | NaN | 1148.0 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 3484952 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 1 | 16/12/2020 | Vente | ... | NaN | 0 | 2.0 | Appartement | NaN | 22.0 | 1.0 | S | NaN | 447.0 |
| 3484953 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 1 | 16/12/2020 | Vente | ... | NaN | 0 | 4.0 | Local industriel. commercial ou assimilé | NaN | 100.0 | 0.0 | S | NaN | 447.0 |
| 3484954 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 1 | 16/12/2020 | Vente | ... | NaN | 0 | 3.0 | Dépendance | NaN | 0.0 | 0.0 | S | NaN | 447.0 |
| 3484955 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 1 | 16/12/2020 | Vente | ... | NaN | 0 | 2.0 | Appartement | NaN | 87.0 | 4.0 | S | NaN | 447.0 |
| 3484956 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 1 | 08/10/2020 | Adjudication | ... | NaN | 2 | 2.0 | Appartement | NaN | 21.0 | 1.0 | NaN | NaN | NaN |
3484957 rows × 43 columns
columns_to_keep = ['Date mutation','Nature mutation','Valeur fonciere','Code postal','Commune','Code departement','Code commune','Nombre de lots','Code type local','Type local','Surface reelle bati','Nombre pieces principales','Surface terrain']
fullData['Date mutation'] = pd.to_datetime(fullData['Date mutation'])
fullData['Code departement'] = fullData['Code departement'].astype(str)
fullData = fullData[columns_to_keep]
fullData = fullData.dropna()
fullData['Valeur fonciere'] = pd.to_numeric(fullData['Valeur fonciere'].str.replace(',', '.'))
fullData
| Date mutation | Nature mutation | Valeur fonciere | Code postal | Commune | Code departement | Code commune | Nombre de lots | Code type local | Type local | Surface reelle bati | Nombre pieces principales | Surface terrain | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 11 | 2020-09-01 | Vente | 72000.0 | 1270.0 | COLIGNY | 1 | 108 | 0 | 1.0 | Maison | 35.0 | 2.0 | 381.0 |
| 13 | 2020-06-01 | Vente | 180300.0 | 1000.0 | BOURG-EN-BRESSE | 1 | 53 | 0 | 1.0 | Maison | 75.0 | 4.0 | 525.0 |
| 16 | 2020-03-01 | Vente | 350750.0 | 1000.0 | SAINT-DENIS-LES-BOURG | 1 | 344 | 0 | 3.0 | Dépendance | 0.0 | 0.0 | 1267.0 |
| 17 | 2020-03-01 | Vente | 350750.0 | 1000.0 | SAINT-DENIS-LES-BOURG | 1 | 344 | 0 | 1.0 | Maison | 201.0 | 7.0 | 1267.0 |
| 18 | 2020-03-01 | Vente | 350750.0 | 1000.0 | SAINT-DENIS-LES-BOURG | 1 | 344 | 0 | 1.0 | Maison | 201.0 | 7.0 | 1497.0 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 3484951 | 2020-12-16 | Vente | 1937500.0 | 75004.0 | PARIS 04 | 75 | 104 | 0 | 2.0 | Appartement | 32.0 | 2.0 | 447.0 |
| 3484952 | 2020-12-16 | Vente | 1937500.0 | 75004.0 | PARIS 04 | 75 | 104 | 0 | 2.0 | Appartement | 22.0 | 1.0 | 447.0 |
| 3484953 | 2020-12-16 | Vente | 1937500.0 | 75004.0 | PARIS 04 | 75 | 104 | 0 | 4.0 | Local industriel. commercial ou assimilé | 100.0 | 0.0 | 447.0 |
| 3484954 | 2020-12-16 | Vente | 1937500.0 | 75004.0 | PARIS 04 | 75 | 104 | 0 | 3.0 | Dépendance | 0.0 | 0.0 | 447.0 |
| 3484955 | 2020-12-16 | Vente | 1937500.0 | 75004.0 | PARIS 04 | 75 | 104 | 0 | 2.0 | Appartement | 87.0 | 4.0 | 447.0 |
1002019 rows × 13 columns
MUTATIONS = fullData['Nature mutation'].unique()
def plotMutations(mut, data, ax):
for m in MUTATIONS:
temp = data[data['Nature mutation'] == m]
result = temp.groupby(temp['Date mutation'].dt.to_period("M"))['Valeur fonciere'].sum()
result.index = result.index.to_timestamp()
x = result.index
y = result.values
if m == mut:
ax.plot(x, y, color="#0b53c1", lw=2.4, zorder=10)
ax.scatter(x, y, fc="w", ec="#0b53c1", s=60, lw=2.4, zorder=12)
ax.autoscale()
else:
ax.plot(x, y, color="#BFBFBF", lw=1.5)
ax.set_title(mut, fontfamily="Inconsolata", fontsize=14, fontweight=500)
return ax
fig, axes = plt.subplots(2, 3, figsize=(14, 7.5))
for idx, (ax, mut) in enumerate(zip(axes.ravel(), MUTATIONS)):
# Only annotate the first panel
annotate = idx == 0
plotMutations(mut, fullData, ax)
On remarque que la plupart des mutations au cours de l'année sont des ventes
data1 = fullData[fullData['Nature mutation'] =='Vente']
data1 = data1.groupby(by='Date mutation',sort='Date mutation')['Valeur fonciere'].count()
#data1.index = data1.index.to_timestamp()
data2 = fullData[fullData['Nature mutation'] =='Vente terrain à bâtir']
data2 = data2.groupby(by='Date mutation',sort='Date mutation')['Valeur fonciere'].count()
#data2.index = data2.index.to_timestamp()
data3 = fullData[fullData['Nature mutation'] =='Echange']
data3 = data3.groupby(by='Date mutation',sort='Date mutation')['Valeur fonciere'].count()
#data3.index = data3.index.to_timestamp()
data4 = fullData[fullData['Nature mutation'] =="Vente en l'état futur d'achèvement"]
data4 = data4.groupby(by='Date mutation',sort='Date mutation')['Valeur fonciere'].count()
#data4.index = data4.index.to_timestamp()
data5 = fullData[fullData['Nature mutation'] =='Adjudication']
data5 = data5.groupby(by='Date mutation',sort='Date mutation')['Valeur fonciere'].count()
#data5.index = data5.index.to_timestamp()
data6 = fullData[fullData['Nature mutation'] =='Expropriation']
data6 = data6.groupby(by='Date mutation',sort='Date mutation')['Valeur fonciere'].count()
#data6.index = data6.index.to_timestamp()
plt.figure(figsize=(18,10))
plt.plot(data1.index, data1.values, "r--", color="red")
plt.plot(data2.index, data2.values, "r--", color="blue")
plt.plot(data3.index, data3.values, "r--", color="green")
plt.plot(data4.index, data4.values, "r--", color="yellow")
plt.plot(data5.index, data5.values, "r--", color="purple")
plt.plot(data6.index, data6.values, "r--", color="black")
plt.legend(['Vente','Vente terrain à bâtir', 'Echange',"Vente en l'état futur d'achèvement",'Adjudication','Expropriation'])
plt.title('Nombre de mutations par type au cours des mois, en cumulé')
plt.show()
On peut encore une fois confirmer que le seul type de mutation importante est la vente.
data = fullData.groupby(['Type local'])['Type local'].count()
plt.bar(data.index, data.values)
bars = ['Appartement', 'Dépendance', 'Industriel', 'Maison']
y_pos = np.arange(len(bars))
plt.xticks(y_pos, bars)
plt.title('Nombre de mutations par type de local')
Text(0.5, 1.0, 'Nombre de mutations par type de local')
perc = [f'{i/data.values.sum()*100:5.2f}%' for i in data.values]
lbl = [f'{j[0]} = {j[1]}' for j in zip(data.index, perc)]
squarify.plot(sizes=data.values, label=lbl)
plt.axis('off')
plt.title('Proportion des types de locaux sur le nombre total de mutations')
plt.show()
On remarque que les mutations concernent principalement des maisons et des appartements.
data=fullData[fullData["Surface terrain"]< 5000]
plt.figure(figsize=(18,10))
plt.xticks(rotation=25)
sbn.violinplot(x = "Type local",y="Surface terrain", data=data)
plt.title('Répartition des types de locaux selon la surface de leur terrain')
Text(0.5, 1.0, 'Répartition des types de locaux selon la surface de leur terrain')
data=fullData[(fullData["Surface reelle bati"]< 1000) & (fullData["Surface reelle bati"].notna())]
plt.figure(figsize=(18,10))
plt.xticks(rotation=25)
sbn.violinplot(x = "Type local",y="Surface reelle bati", data=data)
plt.title('Répartition des types de locaux selon leur surface réelle bâtie')
Text(0.5, 1.0, 'Répartition des types de locaux selon leur surface réelle bâtie')
On voit ici que les données des dépendances sont assez peu intéressantes, puisque leur surface réelle bâtie est proche de zéro.
data = fullData[fullData['Valeur fonciere'] < 2000000]
plt.figure(figsize=(18,10))
plt.xticks(rotation=25)
sbn.violinplot(x="Type local",y="Valeur fonciere",data=data)
plt.title('Répartition des types de locaux selon leur valeur foncière')
Text(0.5, 1.0, 'Répartition des types de locaux selon leur valeur foncière')
data = fullData.groupby(['Code departement'])['Nature mutation'].count().sort_values(ascending=True)
plt.figure(figsize=(10,20))
plt.hlines(y=data.index, xmin=0, xmax=data.values, color='purple')
plt.plot(data.values, data.index, "o", color="gold")
# Add titles and axis names
#plt.yticks(data.index, data.index)
plt.title('Nombre de mutations par département')
plt.xlabel('Nombre de mutations')
plt.ylabel('Numéros de départements')
#data.plot.barh()
plt.show()
data1 = fullData.groupby(['Code departement'])['Nature mutation'].count()
data = fullData[((fullData['Nature mutation']=='Vente') & ((fullData['Type local'] == 'Maison') | (fullData['Type local'] == 'Appartement')))]
data = fullData.groupby(['Code departement'])['Nature mutation'].count()
plt.figure(figsize=(10,20))
plt.hlines(y=data1.index, xmin = 0, xmax = data1.values, color='red')
plt.hlines(y=data.index, xmin=0, xmax=data.values, color='skyblue')
plt.plot(data.values, data.index, "o")
plt.plot(data1.values, data1.index, "x", color="white")
# Add titles and axis names
#plt.yticks(data.index, data.index)
plt.title("Nombre de mutations par département (ronds) et nombre de mutations par département en considérant uniquement les ventes d'appartements et de maisons (croix)")
plt.xlabel('Nombre de mutations')
plt.ylabel('Numéros de départements')
#data.plot.barh()
plt.show()
On ne fait ici que confirmer visuellement que les ventes d'appartement et de maisons constituent la majorité écrasante de mutations, avec une variation extrêmement faible pour certains départements.
myscale = None
def mapping_france_folium(data):
map = folium.Map(location=[48.862, 2.346], zoom_start = 5)
departments = f"https://france-geojson.gregoiredavid.fr/repo/departements.geojson"
d = {'Code': data.index, 'Valeur': np.log(data.values)}
da = pd.DataFrame(d)
folium.Choropleth(geo_data=departments,
data=da,
columns=['Code', 'Valeur'],
key_on='properties.code',
fill_color= "PuRd",
fill_opacity=1,
line_opacity=.1).add_to(map)
folium.LayerControl().add_to(map)
return map
def mapping_Paris_circle(data, bigNumbers = False):
map = folium.Map(location = [48.856578, 2.351828], zoom_start = 12)
arr = json.load(open("arrondissements.geojson"))
d = {'Code': data.index, 'Valeur': data.values}
da = pd.DataFrame(d)
for a in arr["features"]:
prop = a["properties"]
temp = da[da['Code'] == prop["c_arinsee"] - 100]
temp = temp['Valeur'].values
folium.Circle(prop["geom_x_y"],
fill=True,
popup = prop["l_ar"],
radius = (temp[0]/1) if not bigNumbers else temp[0]/9000000).add_to(map)
return map
def mapping_Paris(data):
map = folium.Map(location = [48.856578, 2.351828], zoom_start = 12)
arr = json.load(open("arrondissements.geojson"))
d = {'Code': data.index + 100, 'Valeur': np.log(data.values)}
da = pd.DataFrame(d)
da = da[(da['Code'] >= 75100) & (da['Code'] <= 75120)]
myscale = np.linspace(da['Valeur'].min(), da['Valeur'].max(), 10)
folium.Choropleth(geo_data=arr,
data=da,
columns=['Code', 'Valeur'],
key_on='properties.c_arinsee',
fill_color= "PuRd",
threshold_scale=myscale,
fill_opacity=0.8,
line_opacity=.1).add_to(map)
folium.LayerControl().add_to(map)
return map
def mapping_Lyon(data):
map = folium.Map(location = [45.763420, 4.834277], zoom_start = 12)
arr = json.load(open("adr_voie_lieu.json"))
d = {'Code': data.index + 380, 'Valeur': np.log(data.values)}
da = pd.DataFrame(d)
da = da[(da['Code'] >= 69381) & (da['Code'] <= 69389)]
folium.Choropleth(geo_data=arr,
data=da,
columns=['Code', 'Valeur'],
key_on='properties.insee',
fill_color= "PuRd",
threshold_scale=myscale,
fill_opacity=0.8,
line_opacity=.1).add_to(map)
folium.LayerControl().add_to(map)
return map
def mapping_Marseille(data):
map = folium.Map(location = [43.296482, 5.36978], zoom_start = 12)
arr = json.load(open("quartiers-marseille.geojson"))
d = {'Code': data.index + 200, 'Valeur': np.log(data.values)}
da = pd.DataFrame(d)
da = da[(da['Code'] >= 13201) & (da['Code'] <= 13216)]
da['Code'] = da['Code'].astype(int).astype(str)
folium.Choropleth(geo_data=arr,
data=da,
columns=['Code', 'Valeur'],
key_on='properties.DEPCO',
fill_color= "PuRd",
threshold_scale=myscale,
fill_opacity=0.8,
line_opacity=.1).add_to(map)
folium.LayerControl().add_to(map)
return map
data = fullData.groupby(['Code departement'])['Nature mutation'].count()
map = mapping_france_folium(data)
map
On voit ici, en échelle logarithmique, le nombre de mutations par département au cours de l'année.
data = fullData[fullData['Nature mutation'] == 'Vente'].groupby(['Code departement'])['Valeur fonciere'].sum()
map = mapping_france_folium(data)
map
On voit ci-dessus, en échelle logarithmique, la valeur cumulée des ventes par département au cours de l'année.
data = fullData[fullData['Nature mutation'] == 'Vente']
data = data[data['Type local'] == 'Maison'].groupby(['Code departement'])['Valeur fonciere'].sum()
map = mapping_france_folium(data)
map
On voit ci-dessus, en échelle logarithmique, la valeur cumulée des ventes de maisons par département au cours de l'année. Il est intéressant de noter que l'importance de Paris dans la carte précédente disparaît : très peu de maisons sont vendues à Paris même.
data = fullData[(fullData['Nature mutation'] == 'Vente') & ((fullData['Type local'] == 'Maison') | (fullData['Type local'] == 'Appartement'))]
data['prix_m2'] = data['Valeur fonciere']/data['Surface reelle bati']
data = data.groupby(['Code departement'])['prix_m2'].mean()
#data = data.to_frame()
map = mapping_france_folium(data)
map
On voit ci-dessus, en échelle logarithmique, le prix au m2 par département. On remarque une certaine corrélation entre le nombre de ventes par département et le prix au m2, qu'on cherchera à confirmer par la suite
data = fullData[['Surface reelle bati','Valeur fonciere']]
plt.figure(figsize=(18,10))
plt.scatter(data['Surface reelle bati'],data['Valeur fonciere'])
plt.title('Répartition de la valeur foncière en fonction de la surface bâtie')
plt.show()
On cherche ici à montrer l'existence de valeurs extrêmes dans les données, qui nous ont forcé à adopter une échelle logarithmique pour les cartes, sans quoi nous aurions dû filtrer ces valeurs extrême (ci-dessous).
data = fullData[['Surface reelle bati','Valeur fonciere']]
data = data[data['Valeur fonciere'] < 1000000]
data = data[data['Surface reelle bati'] < 2000]
plt.figure(figsize=(18,10))
plt.scatter(data['Surface reelle bati'],data['Valeur fonciere'])
plt.title('Répartition de la valeur foncière en fonction de la surface bâtie, sans les valeurs extrêmes')
plt.show()
data = fullData.groupby(['Code postal'])['Nature mutation'].count()
map = mapping_Paris_circle(data, False)
map
On voit ci-dessus le nombre de mutations par arrondissement de Paris. Ci-dessous, on représente les mêmes données sur échelle logarithmique.
data = fullData.groupby(['Code postal'])['Nature mutation'].count()
map = mapping_Paris(data)
map
data = fullData[fullData['Nature mutation'] == 'Vente']
data = data.groupby(['Code postal'])['Valeur fonciere'].sum()
map = mapping_Paris_circle(data, True)
map
On voit ci-dessus la valeur des ventes par arrondissement de Paris. Ci-dessous, on représente les mêmes données sur échelle logarithmique.
On va s'intéresser par la suite à la valeur des ventes par arrondissement entre Paris, Lyon et Marseille. On pourra observer que la localisation des quartiers les plus recherchés est bien différente entre ces grandes villes.
data = fullData[fullData['Nature mutation'] == 'Vente']
data = data.groupby(['Code postal'])['Valeur fonciere'].sum()
map = mapping_Paris(data)
map
data = fullData[fullData['Nature mutation'] == 'Vente']
data = data.groupby(['Code postal'])['Valeur fonciere'].sum()
map = mapping_Marseille(data)
map
data = fullData[fullData['Nature mutation'] == 'Vente']
data = data.groupby(['Code postal'])['Valeur fonciere'].sum()
map = mapping_Lyon(data)
map
data = fullData[['Valeur fonciere','Surface reelle bati','Surface terrain', 'Nombre pieces principales']]
sbn.heatmap(data.corr(), annot= True, cmap='Reds')
plt.xticks(rotation = 45)
plt.title('Corrélation entre la valeur foncière, la surface réelle bâtie, \nla surface du terrain et le nombre de pièces principales\n(en France))')
plt.show()
data = fullData[fullData['Code departement'] == '75']
data = data[['Valeur fonciere','Surface reelle bati','Surface terrain', 'Nombre pieces principales']]
sbn.heatmap(data.corr(), annot= True, cmap='Reds')
plt.xticks(rotation = 45)
plt.title('Corrélation entre la valeur foncière, la surface réelle bâtie, \nla surface du terrain et le nombre de pièces principales\n(en IDF))')
plt.show()
On remarquera une grande différence entre les données de corrélation en France, et en Île de France. Particulièrement entre la valeur foncière, la surface réelle bâtie, et la surface du terrain.